/**
 * @file encode.cpp
 * @brief Кодирование между различными кодовыми страницами и Юникодом
 * @version 1.0
 * @date 12 августа 2011
 * @author Владимир Керимов
 * @mainpage Строки и символы Unicode
 * <H3>Кодирование между различными кодовыми страницами и Юникодом</H3>
 * <P>Для использования строк и символов Юникода необходимо подключить файл
 * <BR>#include <qua/ustring.h>
 * <P>Для доступа к функционалу кодирования необходимо подключить файл
 * <BR>#include <qua/encode.h>
 */

#include <qua/ustring.h>
#include <qua/encode.h>
#include <qua/encode/cp866.h>
#include <qua/encode/cp1250.h>
#include <qua/encode/cp1251.h>
#include <qua/encode/cp1252.h>
#include <stdexcept>
#include <algorithm>
#include <ctype.h>
#include <cstring>
#include <string>
#include <map>

namespace qua
{
    namespace
    {
        encode::codepage default_codepage = encode::cpNONE;

        const int utf8_limit[] = { 0x07FF, 0xFFFF, 0x1FFFFF, 0x3FFFFFF };
        const int utf8_shift[] = { 0, 6, 12, 18, 24, 30 };
        const int utf8_mask1[] = { 0x07C0, 0xF000, 0x1C0000, 0x03000000, 0x40000000 };
        const int utf8_mask2[] = { 0x003F, 0x0FC0, 0x03F000, 0x00FC0000, 0x3F000000 };
        const int utf8_head1[] = { 0xC0, 0xE0, 0xF0, 0xF8, 0xFC };
        const int utf8_head2   =   0x80;
        const int utf8_part1[] = { 0x1F, 0x0F, 0x07, 0x03, 0x01 };
        const int utf8_part2   =   0x3F;

        std::map< std::string, encode::codepage > codepage_names;
        std::map< encode::codepage, std::string > codepage_codes;

        void fill_codepage_names()
        {
            codepage_names[ "cp866" ] = encode::cp866;
            codepage_names[ "cp1250" ] = encode::cp1250;
            codepage_names[ "cp1251" ] = encode::cp1251;
            codepage_names[ "cp1252" ] = encode::cp1252;
            codepage_names[ "koi8-r" ] = encode::KOI8R;
            codepage_names[ "utf-8" ] = encode::UTF8;

            for( std::map< std::string, encode::codepage >::iterator it = codepage_names.begin();
                    it != codepage_names.end(); it++ )
            {
                codepage_codes[ it->second ] = it->first;
            }
        }

        typedef std::map< encode::codepage, cp_decoder > cp_match_type;
        typedef cp_match_type::iterator cp_match_it;
        typedef cp_match_type::value_type cp_match_value;

        cp_match_type cp_match;

        void match_decoders()
        {
            cp_match.insert( cp_match_value( encode::cp866,  cp866_decoder() ) );
            cp_match.insert( cp_match_value( encode::cp1250, cp1250_decoder() ) );
            cp_match.insert( cp_match_value( encode::cp1251, cp1251_decoder() ) );
            cp_match.insert( cp_match_value( encode::cp1252, cp1252_decoder() ) );
        }

        inline cp_decoder& get_decoder( encode::codepage cp )
        {
            if( cp == encode::cpDEFAULT ) cp = encode::get_default_codepage();
            if( cp_match.empty() ) match_decoders();
            cp_match_it it = cp_match.find( cp );
            if( it == cp_match.end() )
                throw std::invalid_argument( "Invalid code page argument!" );
            return it->second;
        }
    }

    char encode::to_char( int code, encode::codepage cp )
    {
        if( is_ascii( code ) ) return (char)code;
        return get_decoder( cp ).to_char( code );
    }

    int encode::to_unicode( char sign, encode::codepage cp )
    {
        if( is_ascii( sign ) ) return (int)sign;
        return get_decoder( cp ).to_unicode( sign );
    }

    void encode::set_default_codepage( encode::codepage cp )
    {
        switch( cp )
        {
        case cp866:
        case cp1250:
        case cp1251:
        case UTF8:
            default_codepage = cp;
            break;
        default:
            throw std::invalid_argument( "Invalid code page to set as default!" );
        }
    }

    encode::codepage encode::get_default_codepage()
    {
        if( default_codepage == encode::cpNONE )
            throw std::runtime_error( "Default code page has not been initialized!" );
        return default_codepage;
    }

    encode::codepage encode::read_codepage( const char* name )
    {
        std::string id( std::strlen( name ), '\0' );
        std::transform( name, name + id.size(), &id[0], tolower );
        std::map< std::string, encode::codepage >::iterator cpit;
        if( codepage_names.empty() ) fill_codepage_names();
        cpit = codepage_names.find( id );
        if( cpit == codepage_names.end() )
            throw std::invalid_argument( "Unknown code page name: \"" + std::string( name ) + "\"!" );
        return cpit->second;
    }

    const char* encode::codepage_name( encode::codepage cp )
    {
        std::map< encode::codepage, std::string >::iterator it;
        if( codepage_codes.empty() ) fill_codepage_names();
        it = codepage_codes.find( cp );
        if( it == codepage_codes.end() )
            throw std::invalid_argument( "Such code page does not described!" );
        return it->second.c_str();
    }

    int encode::read_utf8( const char* seq, int& res )
    {
        if( is_ascii( *seq ) )
        {
            res = (int)*seq;
            return 1;
        }

        if( (*seq & 0xC0) != 0xC0 )
            throw std::invalid_argument( "Expected 11xxxxxx at first byte of UTF-8!" );

        int id, bit;
        for( id = 0, bit = 0x20; id < 5 && (*seq & bit) != 0; id++, bit >>= 1 );

        if( id >= 5 )
            throw std::invalid_argument( "Expected zero in 11.....x at first byte of UTF-8!" );

        res = ( *seq++ & utf8_part1[id] ) << utf8_shift[id+1];
        for( int k = id; k >= 0; k-- )
            res |= ( *seq++ & utf8_part2 ) << utf8_shift[k];

        return id + 2;
    }

    int encode::to_utf8( int code, char* res )
    {
        if( code < 0 )
            throw std::invalid_argument( "Code of UTF-8 can't be negative!" );

        if( is_ascii( code ) )
        {
            *res = (char)code;
            return 1;
        }

        int id;
        for( id = 0; id < 4 && code < utf8_limit[id]; id++ );

        *res++ = (char)( utf8_head1[id] | ( ( code & utf8_mask1[id] ) >> utf8_shift[id+1] ) );
        for( int k = id; k >= 0; k-- )
            *res++ = (char)( utf8_head2 | ( ( code & utf8_mask2[k] ) >> utf8_shift[k] ) );

        return id + 2;
    }
}
